grammar EGQL;

options {
  language = Java;
  output=AST;
}

@header {
  package org.yaac.server.egql.antlr;
  
  // exception to throw
  import org.yaac.shared.YaacException;
  // statements and clauses for query construction
  import org.yaac.server.egql.*;
  // evaluators for expression parsing
  import org.yaac.server.egql.evaluator.*;
}

@lexer::header {
  package org.yaac.server.egql.antlr;
  
  // exception to throw
  import org.yaac.shared.YaacException;
  // statements and clauses for query construction
  import org.yaac.server.egql.*;
  // evaluators for expression parsing
  import org.yaac.server.egql.evaluator.*;
}

@members {
  // throw YaacException upon parsing exception
  // http://www.antlr.org/wiki/display/ANTLR3/Error+reporting+and+recovery
  public void displayRecognitionError(String[] tokenNames,
                                      RecognitionException e) {
    String hdr = getErrorHeader(e);
    String msg = getErrorMessage(e, tokenNames);
    throw new YaacException(null, hdr+" "+msg);
  }
}

@lexer::members {
  // make the lexer exit upon first lexical error
  public Token nextToken() {
    while (true) {
      state.token = null;
      state.channel = Token.DEFAULT_CHANNEL;
      state.tokenStartCharIndex = input.index();
      state.tokenStartCharPositionInLine = input.getCharPositionInLine();
      state.tokenStartLine = input.getLine();
      state.text = null;
      if ( input.LA(1)==CharStream.EOF ) {
                Token eof = new CommonToken((CharStream)input,Token.EOF,
                                            Token.DEFAULT_CHANNEL,
                                            input.index(),input.index());
                eof.setLine(getLine());
                eof.setCharPositionInLine(getCharPositionInLine());
                return eof;
      }
      try {
        mTokens();
        if ( state.token==null ) {
          emit();
        }
        else if ( state.token==Token.SKIP_TOKEN ) {
          continue;
        }
        return state.token;
      }
      catch (MismatchedRangeException re) {
        reportError(re);
        throw new YaacException(null, re.getMessage());
      }
      catch (MismatchedTokenException re) {
        reportError(re);
        throw new YaacException(null, re.getMessage());
      }
      catch (RecognitionException re) {
        reportError(re);
        throw new YaacException(null, re.getMessage());
      }
    }
  }
}

/*------------------------------------------------------------------
 * PARSER RULES
 *------------------------------------------------------------------*/

command returns [Command cmd]:
  {$cmd = new Command();} 
  (statement {$cmd.add($statement.stmt, $statement.text);})* EOF!
;

statement returns [Statement stmt] 
  : insert_statement {$stmt = $insert_statement.stmt;}
  | select_statement {$stmt = $select_statement.stmt;}
  | update_statement // TODO
  | delete_statement // TODO
;

insert_statement returns [InsertStatement stmt] :
  {$stmt = new InsertStatement();}
  INSERT (INTO IDENTITY {$stmt.withKind($IDENTITY.text);} )? 
  (VALUES '(' 
    i1=insert_item {$stmt.withItem($i1.item);}
    (',' i2=insert_item {$stmt.withItem($i2.item);} )* 
  ')')?
  (FROM '(' select_statement ')' {$stmt.withSelectStatement($select_statement.stmt);} )?
;

insert_item returns [InsertItem item]:
  IDENTITY '=' bool_exp ('(' INDEXED '=' BOOLEAN ')')?
  {$item = new InsertItem($IDENTITY.text, $bool_exp.e, $BOOLEAN.text);}
;

/**
  select command related
**/
select_statement returns [SelectStatement stmt] :
  {$stmt = new SelectStatement(); }
  select_clause {$stmt.setSelectClause($select_clause.e); }
  from_clause {$stmt.setFromClause($from_clause.e); }
  (where_clause {$stmt.setWhereClause($where_clause.e); } )? 
  (order_clause)?
  (group_clause {$stmt.setGroupByClause($group_clause.e); } )?
  (having_clause {$stmt.setHavingClause($having_clause.e); } )?
;

update_statement :
  UPDATE // TODO
;

delete_statement :
  DELETE  // TODO
;

select_clause returns [SelectClause e]:
  {$e = new SelectClause();}
  SELECT e1=bool_exp {$e.add($e1.e); } 
  (',' e2=bool_exp {$e.add($e2.e);} )*
;

from_clause returns [FromClause e]:
  {$e = new FromClause();}
  FROM entityName=IDENTITY (AS entityAlias=IDENTITY)? 
  {$e.add(new FromEntity($entityName.text, $entityAlias.text));} 
  (',' entityName2=IDENTITY (AS entityAlias2=IDENTITY)? 
  {$e.add(new FromEntity($entityName2.text, $entityAlias2.text));}
  )*
;

where_clause returns [WhereClause e]:
  WHERE bool_exp {$e = new WhereClause($bool_exp.e); }
;

order_clause:
  ORDER BY IDENTITY (ASC|DESC)? (',' IDENTITY (ASC|DESC)?)*
;

group_clause returns [GroupByClause e]:
  {$e = new GroupByClause();}
  GROUP BY (i1=IDENTITY {$e.add($i1.text);} | a1=ALIAS {$e.add($a1.text);}) 
  (',' (i2=IDENTITY {$e.add($i2.text);} | a2=ALIAS {$e.add($a2.text);})) *
;

having_clause returns [HavingClause e]:
  HAVING bool_exp {$e = new HavingClause($bool_exp.e); }
;

// numerical expression
term returns [Evaluator e] 
  : (IDENTITY DOT)? ASTERISK {$e = new FieldEvaluator($text);}
  | IDENTITY {$e = new FieldEvaluator($IDENTITY.text);}
  | ALIAS {$e = new FieldEvaluator($ALIAS.text);}
  | d=DECIMAL {$e = new DecimalEvaluator($d.text);}
  | STRING_LITERAL {$e = new StringEvaluator($STRING_LITERAL.text);}
  | BOOLEAN {$e = new BooleanEvaluator($BOOLEAN.text);}
  | '(' bool_exp ')' {$e = $bool_exp.e; $e.setText("(" + $bool_exp.e.getText() + ")");}
  | aggregation {$e = $aggregation.e;}
  | function {$e = $function.e;}
;

aggregation returns [Evaluator e]:
  AGGREGATION '(' bool_exp ')' {$e = new AggregationEvaluator($AGGREGATION.text, $bool_exp.e); $e.setText($text);}
;

function returns [Evaluator e]:
  FUNCTION {FunctionEvaluator r = new FunctionEvaluator($FUNCTION.text);} 
  '(' ( op1=bool_exp {r.add($op1.e);} (',' op2=bool_exp {r.add($op2.e);} )* )? ')'
  {$e = r; $e.setText($text);} 
;

unary returns [Evaluator e]
  : { boolean positive = true; }
    ('+' | '-' { positive = !positive; })* term
    {
      $e = $term.e;
      if (!positive) {
        $e = new NegationEvaluator($e);
        $e.setText($text);
      }
    }
;

mult returns [Evaluator e]
  : op1=unary { $e = $op1.e; }
    ( '*' op2=unary { $e = new MultiplyEvaluator($e, $op2.e); $e.setText($text);}
    | '/' op2=unary { $e = new DivideEvaluator($e, $op2.e); $e.setText($text);}
    | '%' op2=unary { $e = new ModEvaluator($e, $op2.e); $e.setText($text);}
    )*
;

// so far all arithmetical
expression returns [Evaluator e]
  : op1=mult { $e = $op1.e; }
    ( '+' op2=mult { $e = new AddEvaluator($e, $op2.e); $e.setText($text);}
    | '-' op2=mult { $e = new SubtractEvaluator($e, $op2.e); $e.setText($text);}
    )*
;

// input is expression, output becomes boolean
relation returns [Evaluator e]
  : op1=expression {$e = $op1.e;} 
  ( '=' op2=expression  {$e = new RelationEvaluator($e, RelationEvaluator.Type.EQUALS, $op2.e); $e.setText($text);}
  | '!=' op2=expression {$e = new RelationEvaluator($e, RelationEvaluator.Type.NOT_EQUALS, $op2.e); $e.setText($text);}
  | '<' op2=expression  {$e = new RelationEvaluator($e, RelationEvaluator.Type.LESS_THAN, $op2.e); $e.setText($text);}
  | '<=' op2=expression {$e = new RelationEvaluator($e, RelationEvaluator.Type.LESS_THAN_OR_EQUALS, $op2.e); $e.setText($text);}
  | '>=' op2=expression {$e = new RelationEvaluator($e, RelationEvaluator.Type.GREATER_THAN_OR_EQUALS, $op2.e); $e.setText($text);}
  | '>' op2=expression  {$e = new RelationEvaluator($e, RelationEvaluator.Type.GREATER_THAN, $op2.e); $e.setText($text);}
  | l=LIKE op2=expression
    { $e = new RelationEvaluator($e, RelationEvaluator.Type.LIKE, $op2.e); 
      $e.setText($op1.e.getText() + "_" + $l.text + "_" + $op2.e.getText()); }
  | n=NOT l=LIKE op2=expression 
    { $e = new RelationEvaluator($e, RelationEvaluator.Type.NOT_LIKE, $op2.e); 
      $e.setText($op1.e.getText() + "_" + $n.text + "_" + $l.text + "_" + $op2.e.getText());}
  | b=BETWEEN op2=expression a=AND op3=expression
    { $e = new BetweenEvaluator($e, $op2.e, $op3.e, true);
      $e.setText($op1.e.getText() + "_" + $b.text + "_" + $op2.e.getText() + "_" + $a.text + "_" + $op3.e.getText());}
  | n=NOT b=BETWEEN op2=expression a=AND op3=expression
    { $e = new BetweenEvaluator($e, $op2.e, $op3.e, false);
      $e.setText($op1.e.getText() + "_" + $n.text + "_" + $b.text + "_" + $op2.e.getText() + "_" + $a.text + "_" + $op3.e.getText());}
  | i=IN {InEvaluator ie = new InEvaluator($op1.e, true);
        ie.appendText($op1.e.getText() + "_" + $i.text + "(");} 
    '(' op2=expression {ie.add($op2.e); ie.appendText($op2.e.getText()); } 
        (',' op3=expression {ie.add($op3.e); ie.appendText("," + $op3.e.getText());} )* 
    ')' {ie.appendText(")"); $e = ie;}
  | n=NOT i=IN {InEvaluator ie = new InEvaluator($op1.e, false);
        ie.appendText($op1.e.getText() + "_" + $n.text + "_" + $i.text + "(");} 
    '(' op2=expression {ie.add($op2.e); ie.appendText($op2.e.getText()); } 
        (',' op3=expression {ie.add($op3.e); ie.appendText("," + $op3.e.getText());} )* 
    ')' {ie.appendText(")"); $e = ie;}
  )*
;

bool_negation returns [Evaluator e]
 :{ boolean positive = true;
    StringBuilder prefix = new StringBuilder();
 }
  (NOT { positive = !positive; prefix.append("not_");} )* relation
  {
    if (positive) {
      $e = $relation.e;
    } else {
      $e = new NotEvaluator($relation.e);
    }
    $e.setText(prefix.toString() + $relation.e.getText()); //replace space with _
  }
;

bool_and returns [Evaluator e]
 : op1=bool_negation {$e = $op1.e;}
 (AND op2=bool_negation 
    { $e = new AndEvaluator($e, $op2.e); 
      $e.setText($op1.e.getText() + "_" + $AND.text + "_" + $op2.e.getText());})* //replace space with _
;

bool_exp returns [Evaluator e]
 : op1=bool_and {$e = $op1.e; } 
 (OR op2=bool_and 
    { $e = new OrEvaluator($e, $op2.e); 
      $e.setText($op1.e.getText() + "_" + $OR.text + "_"  + $op2.e.getText());} )* //replace space with _
;

/*------------------------------------------------------------------
 * CASE INSENSITIVE LEXER RULES
 *------------------------------------------------------------------*/
INSERT : I N S E R T;

INTO : I N T O;

VALUES : V A L U E S;

SELECT : S E L E C T;

UPDATE : U P D A T E;

DELETE : D E L E T E;

FROM : F R O M;

WHERE : W H E R E;

GROUP : G R O U P;

ORDER : O R D E R;

BY : B Y;

AS : A S;

HAVING : H A V I N G;

ASC : A S C;

DESC : D E S C;

INDEXED : I N D E X E D;

BOOLEAN :
  T R U E | F A L S E | Y | N
;

AGGREGATION 
  : C O U N T
  | S U M
  | A V G
  | M A X
  | M I N
;

FUNCTION
  : U C A S E // string
  | L C A S E // string
  | M I D // string
  | L E N // string
  | R O U N D // number
  | F O R M A T // number or date
  | N O W // no param, return a date
  | T Y P E // only for field, returns a type in string
  | D A T E T I M E // only for field, returns a type in date
  // used to convert property types
  | K E Y  // used to create key
  | U S E R // used to create a user based on email
  | G E O P T // used to create a GeoPt based on input
  | N U L L
  | L O N G
  | D O U B L E
  | B L O B K E Y
  | B L O B
  | T E X T
  | I M H A N D L E
  | L I S T
  | S H O R T B L O B
  | P O S T A L A D D R E S S
  | P H O N E N U M B E R
  | L I N K
  | E M A I L
  | C A T E G O R Y
;

// relations functions
NOT: N O T;

AND: A N D;

OR: O R;

IN: I N;

LIKE : L I K E;

BETWEEN : B E T W E E N;

// others
ASTERISK: '*';

/*------------------------------------------------------------------
 * BASIC LEXER RULES
 *------------------------------------------------------------------*/
STRING_LITERAL
  : '\''
    ( '\\' '\''  
    | ~('\''|'\n'|'\r') 
    )*
    '\''  // ends with single quote
;

fragment A : 'a' | 'A';
fragment B : 'b' | 'B';
fragment C : 'c' | 'C';
fragment D : 'd' | 'D';
fragment E : 'e' | 'E';
fragment F : 'f' | 'F';
fragment G : 'g' | 'G';
fragment H : 'h' | 'H';
fragment I : 'i' | 'I';
fragment J : 'j' | 'J';
fragment K : 'k' | 'K';
fragment L : 'l' | 'L';
fragment M : 'm' | 'M';
fragment N : 'n' | 'N';
fragment O : 'o' | 'O';
fragment P : 'p' | 'P';
fragment Q : 'q' | 'Q';
fragment R : 'r' | 'R';
fragment S : 's' | 'S';
fragment T : 't' | 'T';
fragment U : 'u' | 'U';
fragment V : 'v' | 'V';
fragment W : 'w' | 'W';
fragment X : 'x' | 'X';
fragment Y : 'y' | 'Y';
fragment Z : 'z' | 'Z';
fragment LETTER : 'a'..'z' | 'A'..'Z';

fragment DIGIT : '0'..'9';

fragment DOT : '.';
fragment COLON : ':';

DECIMAL : DIGIT* (DOT DIGIT+)?;

// IDENTITY matches both kind name and field name
IDENTITY 
  : (LETTER | '_') (LETTER | DIGIT | '_')*
;

ALIAS : IDENTITY DOT IDENTITY;

WHITESPACE : ( '\t' | ' ' | '\r' | '\n' | '\f' )+  { $channel = HIDDEN; };

COMMENT : '//' .* ('\n'|'\r') { $channel = HIDDEN; };

MULTILINE_COMMENT : '/*' .* '*/' { $channel = HIDDEN; };